kim.zhang

风在前,无惧!


  • 首页

  • 标签42

  • 分类12

  • 归档94

  • 搜索

发表于 2021-11-21
本文字数: 5.6k 阅读时长 ≈ 5 分钟

1. 禁止多端用户登陆

在Spring Security中,禁止多端用户登陆有两种方式:

  1. 后来的登陆踢掉已经登陆的用户
  2. 已经登陆的用户,后来的登陆不被允许

1.1 踢掉已经登陆的用户

这两种方式在Spring Security中都很好实现,只需要配置一下sessionManager:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/hello").authenticated()
.antMatchers("/admin").fullyAuthenticated()
.antMatchers("/remember").rememberMe()
.and()
.formLogin()
.permitAll()
.and()
.csrf()
.disable()
.sessionManagement()
// 一个用户最多只允许一端登陆
.maximumSessions(1);
}

当完成上面的配置后,后来的登陆会踢掉已经登陆的用户,当已经登陆的用户再次访问接口时,会报错如下:

1
This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).

可以看到,这里说这个 session 已经过期,原因则是由于使用同一个用户进行并发登录。

1.2 后来的登陆不被允许

另外一种方式,后来的登陆不被允许的设置也非常简单,添加一个配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/hello").authenticated()
.antMatchers("/admin").fullyAuthenticated()
.antMatchers("/remember").rememberMe()
.and()
.formLogin()
.permitAll()
.and()
.csrf()
.disable()
.sessionManagement()
.maximumSessions(1)
// 阻止后来的登陆
.maxSessionsPreventsLogin(true);
}

由于Spring Security是监听session来清理session的记录的。而用户从不同的浏览器登陆后,都会有不同的session,当用户注销登陆之后,session就会失效,但是默认的失效是调用StandardSession#invalidate方法来清理session的。这一个失效事件无法被Spring容器感知到,进而导致用户注销登陆后,session信息没有及时清理,进而导致用户无法重新登陆进来。所以,我们需要一个事件发布器来监听session的事件:

1
2
3
4
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}

当完成以上配置后,如果用户已经登陆过,后来的登陆不被允许,会报错。

2. 源码分析

首先我们知道,在用户登录的过程中,会经过 UsernamePasswordAuthenticationFilter,而 UsernamePasswordAuthenticationFilter 中过滤方法的调用是在 AbstractAuthenticationProcessingFilter 中触发的,我们来看下 AbstractAuthenticationProcessingFilter#doFilter 方法的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}

在这段代码中,我们可以看到,调用 attemptAuthentication 方法走完认证流程之后,回来之后,接下来就是调用 sessionStrategy.onAuthentication 方法,这个方法就是用来处理 session 的并发问题的。具体在ConcurrentSessionControlAuthenticationStrategy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
// 1.获取当前用户的所有session,第一个参数是当前用户的authentication,第二个参数false表示不包含已经过期的session。在用户登陆之后,会将用户的session保存起来,key是principal,value是对应的sessionid的集合(Set<sessionId>)
List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
// 当前用户的有效session数
int sessionCount = sessions.size();
// 设置的session并发数
int allowedSessions = getMaximumSessionsForThisUser(authentication);
// 如果当前有效的session数小于session并发数,不做任何处理
if (sessionCount < allowedSessions) {
// They haven't got too many login sessions running at present
return;
}
// 如果当前session的并发数为-1,表示对session数量不做任何限制
if (allowedSessions == -1) {
// We permit unlimited logins
return;
}
// 如果当前 session 数(sessionCount)等于 session 并发数(allowedSessions),那就先看看当前 session 是否不为 null,并且已经存在于 sessions 中了,如果已经存在了,那都是自家人,不做任何处理;如果当前 session 为 null,那么意味着将有一个新的 session 被创建出来,届时当前 session 数(sessionCount)就会超过 session 并发数(allowedSessions)。
if (sessionCount == allowedSessions) {
HttpSession session = request.getSession(false);
if (session != null) {
// Only permit it though if this request is associated with one of the
// already registered sessions
for (SessionInformation si : sessions) {
if (si.getSessionId().equals(session.getId())) {
return;
}
}
}
// If the session is null, a new one will be created by the parent class,
// exceeding the allowed number
}
// 如果前面的代码都没能return,将进入策略判断方法
allowableSessionsExceeded(sessions, allowedSessions, this.sessionRegistry);
}

allowableSessionsExceeded 方法中,首先会有 exceptionIfMaximumExceeded 属性,这就是我们在 SecurityConfig 中配置的 maxSessionsPreventsLogin 的值,默认为 false,如果为 true,就直接抛出异常,那么这次登录就失败了(对应 1.1 小节踢掉已经登陆用户的效果),如果为 false,则对 sessions 按照请求时间进行排序,然后再使多余的 session 过期即可(对应 1.2 小节后来的登陆不被允许的效果)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions,
SessionRegistry registry) throws SessionAuthenticationException {

if (this.exceptionIfMaximumExceeded || (sessions == null)) {
throw new SessionAuthenticationException(
this.messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed",
new Object[] { allowableSessions }, "Maximum sessions of {0} for this principal exceeded"));
}
// Determine least recently used sessions, and mark them for invalidation
sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
for (SessionInformation session : sessionsToBeExpired) {
session.expireNow();
}
}
一毛也是爱~
Kim.Zhang 微信支付

微信支付

  • 文章目录
  • 站点概览
Kim.Zhang

Kim.Zhang

且行且珍惜
94 日志
12 分类
42 标签
E-Mail Weibo
  1. 1. 1. 禁止多端用户登陆
    1. 1.1. 1.1 踢掉已经登陆的用户
    2. 1.2. 1.2 后来的登陆不被允许
  2. 2. 2. 源码分析
粵ICP备19091267号 © 2019 – 2022 Kim.Zhang | 629k | 9:32
本站总访问量 4 次 | 有 309 人看我的博客啦 |
博客全站共176.7k字
载入天数...载入时分秒...
0%